TerraformでCloudFront Functionsを環境ごとに有効化/無効化してみた
こんにちは、つくぼし(tsukuboshi0755)です!
最近CloudFront Functionsを、Terraformで書いてデプロイしています。
その際に、環境ごとにCloudFront Functionsを自在にON/OFFにしたいと考え、あれこれ調べてみたので共有します。
環境
$ terraform -v Terraform v1.2.3 on darwin_arm64
きっかけ
とあるシステム構築で、検証環境と本番環境に対して、CloudFront + S3の静的Webサイト構成を、Terraformのmoduleを用いてデプロイしていました。
対象のフォルダ構造は、以下の通りです。(一部簡略化してます)
$ tree . ├── env │ ├── prod │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variable.tf │ └── test │ ├── main.tf │ ├── outputs.tf │ └── variable.tf └── modules ├── cloudfront │ ├── main.tf │ ├── outputs.tf │ └── variable.tf └── s3 ├── main.tf ├── outputs.tf └── variable.tf
その内に、検証環境でBasic認証をCloudFront Functionsで実装する必要があり、以下の記事を参考にしながら検討を始めました。
Terraformでの実装
CloudFront Functionsを実装し、ディストリビューションに紐づけるTerraformコードは以下となります。
resource "aws_cloudfront_function" "example" { name = "example" runtime = "cloudfront-js-1.0" comment = "example function" publish = true code = file("../../src/basic_auth.js") } resource "aws_cloudfront_distribution" "example" { default_cache_behavior { # ... other configuration ... function_association { event_type = "viewer-request" function_arn = aws_cloudfront_function.example.arn } } }
function handler(event) { var request = event.request; var headers = request.headers; // echo -n user:pass | base64 var authString = "Basic Y2xhc3NtZXRob2Q6MDkxMmNt"; if ( typeof headers.authorization === "undefined" || headers.authorization.value !== authString ) { return { statusCode: 401, statusDescription: "Unauthorized", headers: { "www-authenticate": { value: "Basic" } } }; } return request; }
今回検証環境でしかBasic認証を使わないため、「検証環境ではディストリビューションにFunctionsを紐づけ、本番環境ではディストリビューションにFunctionsを紐付けない」という動作を、同じCloudFront Moduleを用いて実現しようと考えました。
以上の動作を実現するために、当初は以下の順番での実装を計画しました。
①aws_cloudfront_function
をFunctions Moduleに分離し、検証環境のみFunctions Moduleを呼び出すように設定する
②CloudFrontのmodule内に存在するfunction_association
について、検証環境では呼び出し、本番環境では無視するように設定する
問題点の発覚
しかし実装を進めた所、この②のfunction_association
が予想以上に曲者である事が分かりました。
というのも、aws_cloudfront_distribution
のように、resource単位であればmoduleとして分離する事が可能です。
しかしfunction_association
はaws_cloudfront_distribution
内に他の設定と一緒に存在しているため、moduleとして分離する事ができません。
# 以下のresource全体であれば、moduleとして分離可能 resource "aws_cloudfront_distribution" "example" { default_cache_behavior { # ... other configuration ... # 以下のようにresourceの一部分だけの場合、moduleとしては分離不可 function_association { event_type = "viewer-request" function_arn = aws_cloudfront_function.example.arn } } }
また、terraformにはif文の代わりに三項演算子があり、単一の変数であれば以下のような形で条件分岐させる事が可能です。
resource "aws_instance" "example" { # ... other configuration ... instance_type = "${var.env == "test" ? "t3.large" : "m6i.large"}" }
しかしfunction_association
については、対象内にevent_type
やfunction_arn
といった複数の変数が存在するため、三項演算子だけではON/OFFにする事ができません。
Terraform公式でfunction_association
を1つのresourceとして切り出してもらえれば、moduleを分離するだけで済むので悩む必要がないんですけどね。。。
とはいえ何とか今のバージョンのTerraform(1.2.3)で対応できないか検討し、対応方法を見つけました!
対処方法
ずばりDynamic Blockを用いる事で、環境ごとのディストリビューションにFunctionsを紐づけるor紐付けない事を選択できます。
具体的には、以下のようなコードを書けばOKです!
resource "aws_cloudfront_distribution" "example" { default_cache_behavior { # ... other configuration ... dynamic "function_association" { for_each = var.environment == "test" ? { sample : "cm" } : {} content { event_type = var.cf_event_type function_arn = var.cf_function_arn } } } }
variable "environment" {} variable "cf_function_arn" { default = null } variable "cf_event_type" { default = null }
resource "aws_cloudfront_function" "example" { name = "example" runtime = "cloudfront-js-1.0" comment = "example function" publish = true code = file("../../src/basic_auth.js") }
module "cloudfront" { source = "../../modules/cloudfront" environment = "test" # ... other configuration ... cf_event_type = "viewer-request" cf_function_arn = module.functions.cf_function_arn } module "functions" { source = "../../modules/functions" # ... other configuration ... }
module "cloudfront" { source = "../../modules/cloudfront" environment = "prod" # ... other configuration ... # cf_event_type = "viewer-request" # cf_function_arn = module.functions.cf_function_arn } # 本番環境ではFunctions Moduleは記載しない
上記を実装する事で、Terraformを適用した際に、検証/本番環境ごとに以下のような動作となります。
検証環境の場合
変数environment
の値がtest
で設定されています。
そのためCloudFront ModuleにおけるDynamic Block内のvar.environment == "test"
の条件文を満たし、key valueが1組のmapである{ sample : "cm" }
が返されます。
for_eachに1組のmapが返される事で、function_association
が1個だけ作成されます。
その際に、content
内に存在するevent_type
及びfunction_arn
を適切に設定する事で、Basic認証を実行するCloudFront Functionsがディストリビューションに紐づけられます。
本番環境の場合
変数environment
の値がprod
で設定されています。
そのためCloudFront ModuleにおけるDynamic Block内のvar.environment == "test"
の条件文を満たさず、空のmapである{}
が返されます。
for_eachに空のmapが返される事で、function_association
は作成されません。
content
内に存在するevent_type
及びfunction_arn
もnull
で設定されているため、CloudFront Functionsはディストリビューションに紐づけられません。
こんな感じで、環境ごとにCloudFront FunctionsをON/OFFに切り替える事ができます!
ただし、Dynamic Blockを使用しすぎると、コードの可読性が落ちてしまいがちなのでご注意ください。
最後に
TerraformのDynamic Blockって今まであまり使った事がなかったのですが、このような場面で使用できるのは便利ですね。
今回の使い方以外にも、本来Dynamic Blockはresource内のブロックを動的に定義する事ができるので、今後もどんどん使用していきたいと思います!
以上、つくぼしでした!